Next.js 的 pre-rendering 實作是這個框架的一大賣點,在 Next.js 中的 pre-rendering 有兩種實作方法,一個是 Server Side Rendering,另一個是 Static Side Generation。
在這篇文章中我們要談的是 Static Side Generation,Static Side Generation 指的是在打包階段會將所有渲染所需要的資料都準備好,包括呼叫 API 的資料,最後會將資料都嵌入到 HTML 檔案之中,因此使用者在瀏覽網站時就會直接拿到已經渲染完的 HTML 靜態檔案。
Next.js 提供了一個 function — getStaticProps
,它可以自動地在程式碼打包階段自動執行執行上述的流程,我們不必做過多的設定就可以撰寫 pre-rendering 的程式碼。
getStaticProps
它是一個寫在 React component 外的 function,必須以 export
的形式定義它,而且同時這個 function 也要是 async
的。加上 async
一個很大的好處是,在 getStaticProps
裡面就可以寫 await
,在呼叫 API 時就可以利用這個特性撰寫。
而 getStaticProps
會在打包階段自動執行,並將 props
傳入到 component 中,可以用於渲染 React 中的內容。
export async function getStaticProps(context) {
return {
props: {},
};
}
以下這個範例是在 getStaticProps
中呼叫一個 REST API,從 API 拿到貼文 (Post) 的資料後,傳入到 component 中,並渲染 title
與 body
到畫面上。這個範例我們在前面章節深入淺出 CSR、SSR 與 SSG 也有提到過。
import { GetStaticProps } from "next";
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
interface HomeProps {
post: Post;
}
export default function Home({ post }: HomeProps) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}
export const getStaticProps: GetStaticProps = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const post: Post = await res.json();
return {
props: {
post,
},
};
};
如果上述範例的檔案位置在 pages/home.tsx
,Next.js 會在 next build
的階段為符合的頁面產生 .html
檔案:
.next/server/pages/home.html
getStaticPaths
Dynamic routes 可以匹配近乎是無上限的 pattern,而每一個 pattern 如果在 next build
都要對應到一個頁面,這樣不是會產生無上限的 HTML 檔案嗎?
對於 dynamic routes,Next.js 有相對應的解決方案,也就是使用 getStaticPaths
事先定義哪些頁面需要產生 HTML 檔案。
語法跟 getStaticProps
很像,皆是在 component 外面定義一個 async
的 function,名稱即是 getStaticPaths
,回傳值包含兩個 key,分別是 paths
與 fallback
。
export async const getStaticPaths: GetStaticPaths = () => {
return {
paths: [
{ params: { ... } }
],
fallback: boolean
};
}
paths
paths
這個參數將會決定 dynamic routes 有哪些頁面將會產生 HTML 檔案,例如以在 file-based routing 中我們用到的 pages/products/[id].tsx
,我們可以這樣定義:
return {
paths: [
{ params: { id: '1' } },
{ params: { id: '2' } }
],
fallback: ...
}
以上定義了兩個 id
,所以就會針對兩個頁面 /products/1
與 /products/2
生成 HTML 檔案。
而 dynamic routes 還包括很多種的定義方式:
多層次的定義 pages/posts/[year]/[month]/[day].tsx
,所以在每一個 params
中就要包含 year
、 month
與 day
三個 key:
return {
paths: [
{ params: { year: '2021', month: '7', day: '24' } },
{ params: { year: '2021', month: '9', day: '28' } }
],
fallback: ...
}
catch all routes 的定義方式 pages/posts/[...date].tsx
,而 date
在 router.query
會是 array 的型別,所以在 params
中定義 date
也要是 array 的型別:
return {
paths: [
{ params: { date: ['2021', '7', '24'] } },
{ params: { date: ['2021', '9', '28'] } }
],
fallback: ...
}
還有一種是 optional catch all rotues,例如 pages/posts/[[...date]].tsx
就可以匹配 /posts
、 /posts/123
、 /posts/2021/7/24
多種的路徑,一般情況可以傳入 array 定義路徑,但是也可以使用 null
、 []
、 undefined
或 false
多種不同的方式,讓 Next.js 打包時只產生 /
的頁面:
return {
paths: [
{ params: { date: false } },
],
fallback: ...
}
fallback
fallback
允許傳入三種值,分別為 true
、 false
與 'blocking'
,以下是 Next.js 中的型別定義:
type GetStaticPathsResult<P extends ParsedUrlQuery = ParsedUrlQuery> = {
paths: Array<string | { params: P; locale?: string }>;
fallback: boolean | "blocking";
};
fallback: false
時fallback
為 false
的行為很單純,意思是說當使用者瀏覽沒有定義在 getStaticPaths
中的頁面時,會回傳 404 的頁面。
例如在 pages/products/[id].tsx
中的 getStaticPaths
定義以下的回傳值,所以 Next.js 只會產生 /products/1
與 /products/2
兩個路由相對應的頁面。而使用者如果瀏覽了 /products/3
,他將會收到 404 的頁面。
return {
paths: [{ params: { id: "1" } }, { params: { id: "2" } }],
fallback: false,
};
所以, fallback: false
比較適合用在較為靜態的網站,例如部落格、較小的產品型錄網頁等,只有等網頁的管理者新增內容時,重新讓 Next.js 打包後,才會有新的頁面產生。
fallback: true
時使用 fallback: true
的使用比較複雜一點,因為與 fallback: false
不同的點在於,當使用者瀏覽沒有在 getStaticPaths
中定義的頁面時,Next.js 並不會回應 404 的頁面,而是回應 fallback 的頁面給使用者。
這個流程會呼叫 getStaticProps
,在伺服器產生資料前,使用者瀏覽的是 fallback 的頁面,在 getStaticProps
執行完後,同樣由 props
注入資料到網頁中,使用者這時就能看到完整的頁面。
而經過這個流程的頁面,該頁面會被加入到 pre-rendering 頁面中,下次如果再有同樣頁面的請求時,伺服器並不會再次的重新呼叫 getServerSideProps
,產生新的頁面,而是回應已經產生的頁面給使用者。
使用前幾個章節用到的產品詳細介紹頁面,由於在 getStaticPaths
中的 id
只有 '1'
,所以在 next build
階段只會生成 /products/1
這個頁面的 HTML,但是在設定 fallback: true
的情況下,當一位使用者瀏覽 /products/2
時, Next.js 會做以下幾件事情:
router.isFallback
會一直為 true
,因次可以用條件式渲染 loading 情況下的頁面,而這時從 props
中拿到的 product
是 undefined
。import { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { getProductById, Product as ProductType } from "../../fake-data";
import ProductCard from "../../components/ProductCard";
import { PageTitle, ProductContainer } from "./[id].style";
interface ProductProps {
product: ProductType;
}
const Product = ({ product }: ProductProps) => {
const router = useRouter();
if (router.isFallback) {
return <div>Loading...</div>;
}
return (
<>
<PageTitle>商品詳細頁面</PageTitle>
<ProductContainer>
<ProductCard product={product} all />
</ProductContainer>
</>
);
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const product = getProductById(params?.id as string);
return {
props: {
product,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: [{ params: { id: "1" } }],
fallback: true,
};
};
export default Product;
在使用 fallback: true
的時候,使用者第一次瀏覽網頁時,收到的會是在 router.isFallback
底下渲染的資料:
在第二次瀏覽同一個頁面之後,該頁面因為先前已經生成過,所以可以直接回應 HTML 給使用者,在原始碼中就可以看到完整的資料。
各位讀者可以注意到,因為是 SSG 的關係, fallback: true
實際上是真的會產生 HTML 在資料夾中,例如使用者如果瀏覽不存在的 /products/2
,Next.js 就會動態地生成新的 HTML:
.next/server/pages/prodcuts/2.html
fallback: 'blocking'
時在 getStaticPaths
使用這個設定時,跟 fallback: true
一樣,在使用者瀏覽不存的頁面時,伺服器不會回傳 404 的頁面,而是執行 getStaticProps
,走 pre-rendering 的流程。
但是與 fallback: true
不一樣的點在於沒有 router.isFallback
的狀態可以使用,而是讓頁面卡在 getStaticProps
的階段,等待執行完後回傳結果給使用者。
所以使用者體驗會非常像似 getServerSideProps
,但優點是下次使用者再次瀏覽同一個頁面時,伺服器可以直接回傳已經生成的 HTML 檔案,往後甚至可以藉由 CDN 的 cache 提升頁面的載入速度。
您好,在實作您的範例的時候我遇到以下錯誤:
yarn build
Error occurred prerendering page "/product/Product". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: Cannot destructure property 'id' of 'product' as it is undefined.
product/[id].tsx:
import React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { getProductById, Product as ProductType } from "../../data/fake-data";
import { PageTitle, ProductContainer, BackLink } from "../../styles/index.style";
import { GetStaticPaths } from "next";
import { GetStaticProps } from "next";
import ProductCard from "./Product";
interface ProductProps {
product: ProductType;
}
//Step3
const Product = ({ product }: ProductProps) => {
const router = useRouter();
if (router.isFallback){
return <>Loading...</>; //防止因為第一次渲染沒拿到id而出問題
}
return (
<div>
<PageTitle>商品詳細頁面</PageTitle>
<BackLink>
<Link href="/product">
回產品列表
</Link>
</BackLink>
<ProductContainer>
<ProductCard product={product} all />
</ProductContainer>
</div>
);
};
//Step2
export const getStaticProps: GetStaticProps = async ({ params }) => {
const product = getProductById(params?.id as string);
return {
props: { product },
}
}
//Step1
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: [
{ params: { id: '1' } },
{ params: { id: '2' } }
], //預先產生id=1與id=2的頁面
fallback: false //非id=1, id=2的頁面一律404
}
}
export default Product;
product/Product.tsx
import Image from "next/image";
import Link from "next/link";
import { Product as ProductType } from "../../data/fake-data";
import {
Product,
ImageWrapper,
ProductDetail,
ProductTitle,
ProductDescription,
ProductPrice,
} from "../../styles/ProductCard.style";
interface ProductCardProps {
product: ProductType;
all?: boolean;
}
const ProductCard = ({ product, all }: ProductCardProps) => {
const { id, image, title, description, price } = product;
return (
<Product key={id}>
<ImageWrapper>
<Image src={image} alt="product" style={{ objectFit:"cover"}} fill/>
</ImageWrapper>
<ProductDetail>
<Link href={`/product/${id}`} passHref>
<ProductTitle>{title}</ProductTitle>
</Link>
<ProductDescription $all={all}>{description}</ProductDescription>
<ProductPrice>${price}</ProductPrice>
</ProductDetail>
</Product>
);
};
export default ProductCard;
請問是什麼問題導致ProductCard拿不到id呢?